package org.dreamwork.jasmine2.engine;

import org.apache.log4j.Logger;
import org.dreamwork.jasmine2.events.EventException;
import org.dreamwork.jasmine2.javac.JasmineClassLoader;
import org.dreamwork.jasmine2.javac.JasmineCreator2;
import org.dreamwork.jasmine2.javac.JasmineFileInfo;
import org.dreamwork.jasmine2.javac.configuration.ConfigurationParser;
import org.dreamwork.jasmine2.parser.CharsetDetector;
import org.dreamwork.jasmine2.parser.ParseException;
import org.dreamwork.jasmine2.parser.TaglibDirective;
import org.dreamwork.jasmine2.web.controls.Page;
import org.dreamwork.util.StringUtil;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * jamine 引擎。
 * <p/>
 * <p>jasmine 引擎负责整个 jasmine 应用的解析，编译，运行及类/资源加载。
 * <h3>1. jasmine 页面解析</h3>
 * jasmine 页面使用类似于 jsp 的语法，大多数 jsp 语法在 jasmine 页面中都可以使用，但含义有部分变化：
 * <table>
 * <tr valign="top">
 * <td>
 * </tr>
 * </table>
 * <p/>
 * <p/>
 * Created by IntelliJ IDEA.
 * User: seth
 * Date: 2009-1-21
 * Time: 16:54:45
 */
public class JasmineEngine extends HttpServlet {
    private static final Logger logger = Logger.getLogger (JasmineEngine.class);
    public static final String J2EE_TEMP_DIR = "javax.servlet.context.tempdir";

    private File srcdir, outdir, basedir;
    private JasmineClassLoader loader;
    private final Map<String, Set<String>> cache = new ConcurrentHashMap<String, Set<String>> ();

    private String cp;

    private static Set<URL> urls = new HashSet<URL> ();

    public static URL[] getClassURLs () {
        URL[] a = new URL[urls.size ()];
        return urls.toArray (a);
    }

    private HttpContext buildHttpContext (HttpServletRequest req, HttpServletResponse response) throws IOException {
        HttpServletRequest request;
        if (isMultipartContent (req))
            request = new JasmineRequest (req);
        else
            request = req;

        HttpContext context = new HttpContext ();
        context.setApplication (getServletContext ());
        context.setRequest (request);
        context.setResponse (response);
        context.setSession (request.getSession (true));
        return context;
    }

    protected void service (HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
        JasmineFileInfo info = new JasmineFileInfo (req.getServletPath (), srcdir, outdir);
        if (logger.isDebugEnabled ()) {
            logger.debug ("================= Jasmine Page Info ====================");
            logger.debug ("* class name  : " + info.getClassName ());
            logger.debug ("* file name   : " + info.getFileName ());
            logger.debug ("* orignal path: " + info.getOrignalPath ());
            logger.debug ("* package name: " + info.getPackageName ());
            logger.debug ("* package path: " + info.getPackagePath ());
            logger.debug ("* src file    : " + info.getSrcFile ().getAbsolutePath ());
            logger.debug ("* class file  : " + info.getClassFile ());
            logger.debug ("* class dir   : " + info.getClassDir ());
            logger.debug ("=========================================================");
        }

        Page page = null;

        HttpContext context = buildHttpContext (req, response);
        try {
            HttpServletRequest request = context.getRequest ();
            page = getPage (info, request);
            if (page == null) {
                response.sendError (404);
                return;
            }
            request.setCharacterEncoding (page.getPageCharset ());

            // start page life circle
            /*lifeCircle (page, context);*/

        // setting current running context
        page.setHttpContext (context);
        // init page and it's child controls
        page.init ();

        page.preload ();

        // load saved states
        // process posted back data
        // fire OnLoad event
        page.load ();

        // valid all page's child controls
        if (page.validControls (page)) {
            // dispatch client event
            page.dispatchEvent ();
        }



        String url = page.getTarget ();
//        HttpServletRequest request = context.getRequest ();
//        HttpServletResponse response = context.getResponse ();
        if (url != null) {
            int type = page.getGotoType ();
            if (type == 1) {
                RequestDispatcher dispatcher = request.getRequestDispatcher (url);
                ServletRequest r;
                if (request instanceof JasmineRequest)
                    r = ((JasmineRequest) request).getOrigRequest ();
                else
                    r = request;
                dispatcher.forward (r, response);
            } else
                response.sendRedirect (url);

            return;
        }

        page.loadComplete ();

        // create child controls of page
        if (!page.isChildControlsCreated ())
            page.createChildControls ();




            String contentType = page.getContentType ();
            StringWriter sw = null;
            PrintWriter writer = null;
            if (StringUtil.isEmpty (contentType)) {
                contentType = "text/plain;charset=utf-8";
                response.setContentType (contentType);
                writer = response.getWriter ();
            } else if (contentType.startsWith ("text/html")) {
                request.setCharacterEncoding (page.getPageCharset ());
                response.setContentType (contentType);
                writer = response.getWriter ();
            } else if ("application/pdf".startsWith (contentType)) {
                sw = new StringWriter ();
                writer = new PrintWriter (sw, true);
            }

            // render page to response
            page.render (writer);

            if ("application/pdf".startsWith (contentType)) {
                writer.flush ();
                sw.flush ();
                new PDFProcessor (basedir).processPDF (page, sw.getBuffer ().toString ());
            }
            response.flushBuffer ();
        } catch (Throwable t) {
            t.printStackTrace ();
            response.setContentType ("text/html;charset=utf-8");
            logger.error (t.getMessage (), t);
            page = new DefaultErrorPage (t);
            try {
                lifeCircle (page, context);
                page.render (response.getWriter ());
                response.flushBuffer ();
            } catch (EventException e) {
                e.printStackTrace ();
            }
        } finally {
            try {
                // dispose page.
                if (page != null) page.dispose ();
            } catch (Throwable t) {
                logger.error (t.getMessage (), t);
            }
        }
    }

    private void lifeCircle (Page page, HttpContext context) throws EventException, IOException, ServletException {
        // setting current running context
        page.setHttpContext (context);
        // init page and it's child controls
        page.init ();

        page.preload ();

        // load saved states
        // process posted back data
        // fire OnLoad event
        page.load ();

        // valid all page's child controls
        if (page.validControls (page)) {
            // dispatch client event
            page.dispatchEvent ();
        }

        page.loadComplete ();

        String url = page.getTarget ();
        HttpServletRequest request = context.getRequest ();
        HttpServletResponse response = context.getResponse ();
        if (url != null) {
            int type = page.getGotoType ();
            if (type == 1) {
                RequestDispatcher dispatcher = request.getRequestDispatcher (url);
                ServletRequest r;
                if (request instanceof JasmineRequest)
                    r = ((JasmineRequest) request).getOrigRequest ();
                else
                    r = request;
                dispatcher.forward (r, response);
            } else
                response.sendRedirect (url);

            return;
        }

        // create child controls of page
        if (!page.isChildControlsCreated ())
            page.createChildControls ();
    }

    private Page getPage (JasmineFileInfo info, HttpServletRequest request) throws IOException, ParseException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        prepareDirs ();
        PageScaner scaner = new PageScaner (request.getServletPath ());
        List<TaglibDirective> list;

        try {
            list = scaner.scan ();
        } catch (FileNotFoundException ex) {
//            response.sendError (HttpServletResponse.SC_NOT_FOUND);
            return null;
        }

        String currentPath = request.getServletPath ();
        int pos = currentPath.lastIndexOf ("/");
        if (pos != -1) currentPath = currentPath.substring (0, pos);

        Map<String, String> map = new HashMap<String, String> ();
        if (list != null) for (TaglibDirective taglib : list) {
            String userControlFile = currentPath + '/' + taglib.getURI ();
            logger.debug ("find user control: " + userControlFile);
            JasmineFileInfo fileInfo = new JasmineFileInfo (userControlFile, srcdir, outdir);
            synchronized (cache) {
                Set<String> set = cache.get (fileInfo.getClassName ());
                if (set == null) {
                    set = new HashSet<String> ();
                    cache.put (fileInfo.getClassName (), set);
                }
                set.add (info.getClassName ());
            }
            fileInfo.createPackageDir ();
            if (needCreatePage (fileInfo)) {
                synchronized (cache) {
                    Set<String> set = cache.get (fileInfo.getClassName ());
                    if (set != null) for (String name : set) {
                        loader.removeLoader (name);
                    }
                }
                logger.debug ("preparing to create user control " + userControlFile);
                createPage (fileInfo, null);
                logger.debug (userControlFile + " created as " + fileInfo.getClassFile ());
            }
            map.put (taglib.getName (), fileInfo.getClassName ());
        }

        if (needCreatePage (info)) {
            loader.removeLoader (info.getClassName ());
            createPage (info, map.size () == 0 ? null : map);
        }

        return loader.load (info.getClassName ());
    }

    private boolean needCreatePage (JasmineFileInfo info) {
        ServletContext context = getServletContext ();
        String root = context.getRealPath ("/");
        if (!info.getClassFile ().exists ()) return true;
        else if (root != null) {
            File jsp = new File (context.getRealPath (info.getOrignalPath ()));
            if (jsp.lastModified () > info.getClassFile ().lastModified ())
                return true;
        }
        return false;
    }

    private void createPage (JasmineFileInfo info, Map<String, String> userControls) throws IOException, ParseException {
        String charset;
        Reader in;
        ServletContext context = getServletContext ();

        String root = context.getRealPath ("/");
        if (root == null) { // war
            URL url = getClass ().getResource (info.getOrignalPath ());
            URLConnection conn = url.openConnection ();
            charset = new CharsetDetector (url).getCharset ();
            in = new InputStreamReader (conn.getInputStream (), charset);
        } else {
            File jsp = new File (context.getRealPath (info.getOrignalPath ()));
            charset = new CharsetDetector (jsp.toURL ()).getCharset ();
            in = new InputStreamReader (new FileInputStream (jsp), charset);
        }

        JasmineCreator2 creator = new JasmineCreator2 ();
        creator.setCompileClassPath (getClassPath ());
        if (userControls != null) {
            creator.setUserControl (userControls);
            creator.setUserControlClassPath (info.getClassDir ());
        }
        creator.setReader (in);
        creator.create (info);
    }

    public void init () throws ServletException {
        super.init ();

        try {
            ServletContext context = getServletContext ();
            basedir = (File) context.getAttribute (J2EE_TEMP_DIR);
            srcdir = new File (basedir, "src");
            outdir = new File (basedir, "classes");

            ConfigurationParser.parse ();
        } catch (Exception ex) {
            logger.fatal (ex.getMessage (), ex);
            throw new ServletException (ex);
        }
    }

    private void prepareDirs () throws IOException {
        if (!srcdir.exists ()) srcdir.mkdirs ();
        if (!outdir.exists ()) outdir.mkdirs ();
        if (loader == null) loader = new JasmineClassLoader (outdir);
    }

    private String getClassPath () throws MalformedURLException {
/*
        ClassLoader loader = getClass ().getClassLoader ();
        StringBuilder builder = new StringBuilder ();
        while (loader != null) {
            if (loader instanceof URLClassLoader) {
                URLClassLoader ucl = (URLClassLoader) loader;
                for (URL url : ucl.getURLs ()) {
                    if (builder.length () > 0) builder.append (File.pathSeparator);
                    String path = url.getFile ();
                    if (path.charAt (path.length () - 1) == '/')
                        path = path.substring (0, path.length () - 1);
                    String fileName = path.toLowerCase ();
                    if (fileName.endsWith (".jar!") || fileName.endsWith (".zip!"))
                        fileName = path.substring (0, fileName.length () - 1);
                    if (fileName.startsWith ("file:")) {
                        try {
                            URL u = new URL (fileName);
                            fileName = u.getFile ();
                        } catch (MalformedURLException e) {
                            e.printStackTrace ();
                        }
                    }
                    File file = new File (fileName);
                    if (file.exists ())
                        builder.append (file.getAbsolutePath ());
                }
            }
            loader = loader.getParent ();
        }

        builder.append (File.pathSeparator).append (outdir.getAbsolutePath ());

        return builder.toString ();
*/
        if (cp != null) return cp;
        boolean isWindows = System.getProperty ("os.name").toLowerCase ().indexOf ("windows") >= 0;
        Set<String> set = new HashSet<String> ();
        ClassLoader loader = getClass ().getClassLoader ();
        while (loader != null) {
            if (loader instanceof URLClassLoader) {
                for (URL url : ((URLClassLoader) loader).getURLs ()) {
                    String path = url.getPath ();
                    if (path.startsWith ("file:")) path = path.substring (5);
                    if (isWindows && path.charAt (0) == '/') path = path.substring (1);
                    if (path.endsWith ("!/")) path = path.substring (0, path.length () - 2);
                    path = path.replace ("%20", " ");
                    set.add (path);

                    File file = new File (path);
                    urls.add (file.toURL ());
                }
            }
            loader = loader.getParent ();
        }

        StringBuilder builder = new StringBuilder ();
        for (String path : set) {
            if (builder.length () > 0) builder.append (File.pathSeparatorChar);
            builder.append (path);
        }
        return cp = builder.toString ();
    }

    private boolean isMultipartContent (HttpServletRequest request) {
        String contentType = request.getHeader ("content-type");
        return contentType != null && contentType.indexOf ("multipart/form-data") >= 0;
    }
}